package simtester.agent

import geb.*
import groovyx.net.http.ContentType
import java.util.logging.FileHandler
import java.util.logging.Logger
import java.util.logging.SimpleFormatter
import java.util.Random
import org.apache.http.conn.HttpHostConnectException

class Agent {
    def logname
    File finishFile
    Logger log = Logger.getLogger("simtester.agent")
    FileHandler fh
    RESTCli server
    RESTCli server2
    def config
    def repeats
    def firstuser
    def users
    def testcaseId
    long endtime
    
    def type
    int peruser
    def per
    def duration
    def delay
    
    Agent(configFile) {
        config = new ConfigSlurper().parse(configFile.toURI().toURL())
        logname = 'agent.log'
        fh = new FileHandler(logname)
        fh.setFormatter(new AgentLogFormatter())
        log.addHandler(fh)
        
        finishFile = new File('finished')

        type       = 'GUI'
        repeats    = config.repeats ?: 1
        firstuser  = config.firstuser ?: 1
        users      = config.users ?: 1
        testcaseId = config.testcaseid
        
        if (config.duration) {
            endtime = System.currentTimeMillis() + config.duration
        }
        
        // Load test
        if (config.peruser) {
            type     = 'Load'
            peruser  = config.peruser
            per      = config.per
            duration = config.duration
            delay    = config.delay
            endtime  = System.currentTimeMillis() + config.totalduration
        }
        
        server  = new RESTCli(config.serverurl, ContentType.JSON)
        server2 = new RESTCli(config.serverurl, ContentType.JSON)
    }

    def start(scriptFile = null) {
        GroovyObject script = loadScript(scriptFile)
        
        // Run test
        def logSubmitterThread = startLogSubmitterThread(logname)
        
        if (type == 'Load') {
            startLoadTest(script)
        } else {
            startGuiTest(script)
        }

        log.info("Test Completed")
        
        // Create file to indicate test has finished, end submitter
        finishFile.write('')
        logSubmitterThread.join()
        
        completeTestRun()

        log.getHandlers().each { h ->
            h.close()
        }
    }
    
    void startGuiTest(script) {
        def threads = []
        def lastuser = firstuser + users - 1
        (firstuser..lastuser).step(1) { user ->
            threads << Thread.start {
                def counter = 1
                while (true) {
                    printStart(user, counter)

                    boolean success = true
                    def agentLogger = new AgentLogger(testcaseId: testcaseId, user: user, iter: counter)
                    success = runTest(script, user, counter, agentLogger)
                    logOutcome(success, agentLogger)
                    
                    printEnd(user, counter)
        
                    if (endtime) {
                        if (System.currentTimeMillis() >= endtime)
                            break
                    } else if (counter >= repeats) {
                        break
                    }
                    counter++
                }
            }
            sleep 2000
        }
        
        threads.each { it.join() }
    }

    void startLoadTest(script) {
        def threads = []
        
        sleep delay
        
        int userCounter = firstuser
        (0..(duration-1)).step(per) { currentTime ->
            peruser.times { user ->
                int usr = userCounter
                threads << Thread.start {
                    printStart(usr, 1)
                    
                    boolean overallSuccess = true
                    def agentLogger = new AgentLogger(testcaseId: testcaseId, user: usr, iter: 1)
                    while (true) {
                        boolean success = true
                        success = runTest(script, usr, 1, agentLogger)
                        if (overallSuccess && !success) {
                            overallSuccess = false
                        }
                        
                        if (System.currentTimeMillis() >= endtime) {
                            break
                        }
                    }
                    
                    logOutcome(overallSuccess, agentLogger)
                    
                    printEnd(usr, 1)
                    Random rand = new Random()
                    sleep rand.nextInt(20) * 100
                }
                userCounter++
            }
            
            if (currentTime + per > duration-1) {
                sleep duration - currentTime
            } else {
                sleep per
            }
        }
        
        threads.each { it.join() }
    }
    
    void logOutcome(success, logger) {
        if (success) {
            logger.info('SYS:testrun status:success')
        } else {
            logger.info('SYS:testrun status:failed')
        }
    }
    
    boolean runTest(script, user, iteration, logger) {
        boolean success = true
        try {
            script.invokeMethod("runtest", [testcaseId: testcaseId, user: user, iter: iteration, logger: logger])
        } catch(AssertionError e) {
            success = false
            logger.severe("Assertion Error:\n" + e.message)
        } catch(Exception e) {
            success = false
            logger.severe("Exception Caught:\n" + e.message)
        }
        
        return success
    }
    
    private GroovyObject loadScript(scriptFile = null) {
        def file = scriptFile ?: new File('AgentScript.groovy')
        if (!file.exists()) {
            file = new File('src/main/groovy/simtester/agent/AgentScript.groovy')
        }
        
        final GroovyClassLoader classloader = new GroovyClassLoader()
        Class scriptclass = classloader.parseClass(file)

        return (GroovyObject) scriptclass.newInstance()
    }
    
    private startLogSubmitterThread(log) {
        Thread.start {
            boolean finished
            def filePos = 0
            RandomAccessFile logfile

            while (true) {
                finished = finishFile.exists()

                StringBuffer buffer = new StringBuffer()
                try {
                    
                    logfile = new RandomAccessFile(logname, 'r')
                    def fileLength = logfile.length()
                    logfile.seek(filePos)

                    while (true) {
                        def str = logfile.readLine()
                        if (filePos < fileLength) {
                            buffer.append(str)
                            buffer.append("\n")
                            filePos = logfile.filePointer
                        } else {
                            break
                        }
                    }
                } finally {
                    logfile.close()
                }

                if (buffer.length() > 0) {
                    submitTestResults(buffer.toString())
                }

                if (finished) {
                    break
                }

                sleep 3000
            }
        }
    }
    
    //TODO: May want to retry submitting results for a long while
    private def submitTestResults(str) {
        try {
            def results
            results = server2.postNoLog(
                path: "testRun/sendresults/" + config.testrunid,
                body: [results: str])

            if (results.data.msg != 'ok') {
                log.severe(results.data.error)
            }
        } catch(UnknownHostException e) {
            log.severe("Unknown host: ${e.message}")
        } catch(Exception e) {
            log.severe(e.message)
        }
    }
    
    private def completeTestRun() {
        def retries = 5
        boolean success = false
        def results
        for (i in 1..retries) {
            results = sendCompletionRequest()
            if (results.success) {
                break
            }

            if (i < retries) {
                log.info("Retry attempt $i...")
                sleep 1000
            }
        }
        
        if (!results.success) {
            log.severe(results.error)
        }
    }
    
    private def sendCompletionRequest() {
        // Change test run status to "completed"
        log.info('Sending request to complete test execution')
        
        def response
        def results = [success: false]
        try {
            response = server.put(
                path: "testRun/update/" + config.testrunid,
                body: [status: 'completed'])
            
            if (response.data.msg == 'ok') {
                results.success = true
                log.info("Success: status of test execution is now 'completed'")
            } else {
                results.error = response.data.error
                log.info(response.data.error)
            }

        } catch(UnknownHostException e) {
            def msg = "Unknown host: ${e.message}"
            results = [success: false, error: msg]
            log.info(msg)
        } catch(Exception e) {
            results = [success: false, error: e.message]
            log.info(e.message)
        }
        
        return results
    }
    
    private printStart(user, iteration) {
        // Message: "----- Start Test Run $user:$iteration:$testcaseId -----"
        StringBuilder msg = new StringBuilder(64)
        msg.append("----- Start Test Run ")
        msg.append(user)
        msg.append(':')
        msg.append(iteration)

        if (testcaseId) {
            msg.append(':')
            msg.append(testcaseId)
        }
        
        msg.append(" -----")
        log.info(msg.toString())
    }
    
    private printEnd(user, iteration) {
        // Message: "----- End Test Run $user:$iteration:$testcaseId -----"
        StringBuilder msg = new StringBuilder(64)
        msg.append("----- End Test Run ")
        msg.append(user)
        msg.append(':')
        msg.append(iteration)

        if (testcaseId) {
            msg.append(':')
            msg.append(testcaseId)
        }
        
        msg.append(" -----")
        log.info(msg.toString())
    }

    static main(args) {
        System.properties.with { prop ->
            prop['org.apache.commons.logging.Log']='org.apache.commons.logging.impl.SimpleLog'
            prop['org.apache.commons.logging.simplelog.log.com.gargoylesoftware.htmlunit']='FATAL'
        }
        
        def file = new File('conf/AgentConfig.groovy')
        if (!file.exists()) {
            file = new File('AgentConfig.groovy')
        }
        
        def agent = new Agent(file)
        agent.start()
    }

}
